﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ParsedXmlTree.cs" company="" />
//
// <summary>
//  This is a parser to convert XML input into a structured data-set of XML element leafs and their element path as a key.
//  The key contains also the value of a specialized attribute given as a key, if given at all.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Utilities
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Xml.Linq;

    /// <summary>
    /// This is a parser to convert XML input into a structured data-set of XML element leafs and their element path as a key.
    /// The key contains also the value of a specialized attribute given as a key, if given at all.
    /// </summary>
    public class ParsedXmlTree
    {
        #region Constants

        /// <summary>
        /// The character that is used in the string path separating between <see cref="XElement"/>s.
        /// </summary>
        public const char PathSeparator = '/';

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="ParsedXmlTree"/> class, out of a given XML document.
        /// </summary>
        /// <param name="xmlInput">The input XML document for the parsing analysis.</param>
        /// <param name="defaultKeyName">The default key name is the name of the attribute that if found, this attribute is the key for the data in its element.</param>
        /// <param name="keyMap">Maps element path -> attribute name which is a key to the element in that path.</param>
        public ParsedXmlTree(XDocument xmlInput, string defaultKeyName = null, IReadOnlyDictionary<string, string> keyMap = null)
        {
            TreeRecords = new Dictionary<string, ComparableSet<KeyedXmlRecord>>();
            ElementRecords = new Dictionary<XElement, KeyedXmlRecord>();
            InputXml = xmlInput;
            DefaultKeyName = defaultKeyName;
            KeyMap = keyMap;
            IgnoreValueCase = false;
            IgnoreNameCase = false;
            IgnoreWhiteSpaces = false;

            // Parse the given XML document:
            CreateXmlRecords(InputXml.Elements(), new string(PathSeparator, 1));
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ParsedXmlTree"/> class, out of a given XML document.
        /// </summary>
        /// <param name="xmlInput">The input XML document for the parsing analysis.</param>
        /// <param name="settings">Initial settings.</param>
        public ParsedXmlTree(XDocument xmlInput, ParsedXmlTreeInitialSettings settings)
        {
            TreeRecords = new Dictionary<string, ComparableSet<KeyedXmlRecord>>();
            ElementRecords = new Dictionary<XElement, KeyedXmlRecord>();
            InputXml = xmlInput;
            DefaultKeyName = settings.DefaultKeyName;
            KeyMap = settings.KeyMap;
            IgnoreValueCase = settings.IgnoreValueCase;
            IgnoreNameCase = settings.IgnoreNameCase;
            IgnoreWhiteSpaces = settings.IgnoreWhiteSpaces;

            // Parse the given XML document:
            CreateXmlRecords(InputXml.Elements(), new string(PathSeparator, 1));
        }

        /// <summary>
        /// Gets the XML document serving as base to the data set analysis provided by this parser.
        /// </summary>
        public XDocument InputXml { get; private set; }

        /// <summary>
        /// Gets the default key set for this parsed XML.
        /// </summary>
        public string DefaultKeyName { get; private set; }

        /// <summary>
        /// Gets the map of element path -> attribute name which is the key to the element in that path.
        /// </summary>
        public IReadOnlyDictionary<string, string> KeyMap { get; private set; }

        /// <summary>
        /// Gets the map of a represented element's path and the element's record.
        /// </summary>
        public Dictionary<string, ComparableSet<KeyedXmlRecord>> TreeRecords { get; private set; }

        /// <summary>
        /// Gets the map between an XML element and a matching XML record (if exists).
        /// </summary>
        public Dictionary<XElement, KeyedXmlRecord> ElementRecords { get; private set; }

        /// <summary>
        /// Gets whether the value of XML objects is case insensitive.
        /// </summary>
        public bool IgnoreValueCase { get; private set; }

        /// <summary>
        /// Gets whether the names of XML objects is case insensitive.
        /// </summary>
        public bool IgnoreNameCase { get; private set; }

        /// <summary>
        /// Gets whether to ignore the white-spaces of the values of XML objects.
        /// </summary>
        public bool IgnoreWhiteSpaces { get; private set; }

        /// <summary>
        /// If can, creates an addition-comparer out of given two sides of XML information, and a prefix to XML element-path so it can be identified.
        /// The addition comparer, marks the XML attributes that are removed and added on both given XML sides of the required XML node.
        /// Case sensitiveness and white-space handling are decided by this hosting class.
        /// </summary>
        /// <param name="parentXmlPath">The starting path of the XML element in question.</param>
        /// <param name="xmlOnlyOrigin">The map of XML record entries available only in the origin XML describer.</param>
        /// <param name="xmlOnlyTarget">The map of XML record entries available only in the target XML describer.</param>
        /// <param name="additionComparer">OUT: The synthesized comparer showing removes and additions to the given XML attributes descriptions.</param>
        /// <returns>Whether there's anything to compare for addition.</returns>
        public bool CreateAdditionComparer(
            string parentXmlPath,
            IReadOnlyDictionary<string, ComparableSet<KeyedXmlRecord>> xmlOnlyOrigin,
            IReadOnlyDictionary<string, ComparableSet<KeyedXmlRecord>> xmlOnlyTarget,
            out CollectionComparer<XName, string> additionComparer)
        {
            string potentialPath = KeyedXmlRecord.GetElementPathWithoutFirstKey(parentXmlPath);
            string potentialKey = KeyedXmlRecord.GetKeyFromElementPath(parentXmlPath);
            var relevantOnlyOrigin = xmlOnlyOrigin.Where(item => KeyedXmlRecord.GetElementPathWithoutFirstKey(item.Key).Equals(potentialPath)).ToList();
            var relevantOnlyTarget = xmlOnlyTarget.Where(item => KeyedXmlRecord.GetElementPathWithoutFirstKey(item.Key).Equals(potentialPath)).ToList();
            bool needAdditionComparison =
                relevantOnlyOrigin.Count == 1 &&
                relevantOnlyTarget.Count == 1 &&
                (potentialKey.Equals(KeyedXmlRecord.GetKeyFromElementPath(relevantOnlyOrigin.First().Key)) ||
                 potentialKey.Equals(KeyedXmlRecord.GetKeyFromElementPath(relevantOnlyTarget.First().Key)));

            additionComparer = null;

            if (needAdditionComparison)
            {
                var originalAddRecord = relevantOnlyOrigin.First().Value.First();
                var targetAddRecord = relevantOnlyTarget.First().Value.First();

                additionComparer = new CollectionComparer<XName, string>(
                    originalAddRecord.Element.Attributes().ToDictionary(
                        recordAttribute => MiscUtils.AdjustCaption(recordAttribute.Name, IgnoreNameCase, IgnoreWhiteSpaces),
                        recordAttribute => MiscUtils.AdjustCaption(recordAttribute.Value, IgnoreValueCase, IgnoreWhiteSpaces)),
                    targetAddRecord.Element.Attributes().ToDictionary(
                        recordAttribute => MiscUtils.AdjustCaption(recordAttribute.Name, IgnoreNameCase, IgnoreWhiteSpaces),
                        recordAttribute => MiscUtils.AdjustCaption(recordAttribute.Value, IgnoreValueCase, IgnoreWhiteSpaces)));
            }

            return needAdditionComparison;
        }

        /// <summary>
        /// If can, creates a difference-comparer out of a given XML-elements difference information and a prefix to the relevant XML element containing
        /// the attributes to compare.
        /// Case sensitiveness and white-space handling are decided by this hosting class.
        /// </summary>
        /// <param name="parentXmlPath">The starting path of the XML element in question.</param>
        /// <param name="xmlDifferences">The map of XML record entry differences.</param>
        /// <param name="diffComparer">OUT: The synthesized comparer showing attribute-differences.</param>
        /// <returns>Whether there's anything to compare for differences.</returns>
        public bool CreateDifferenceComparer(
            string parentXmlPath,
            IReadOnlyDictionary<string, Tuple<ComparableSet<KeyedXmlRecord>, ComparableSet<KeyedXmlRecord>>> xmlDifferences,
            out CollectionComparer<XName, string> diffComparer)
        {
            var needDiffComparison = xmlDifferences != null && xmlDifferences.ContainsKey(parentXmlPath);

            diffComparer = null;

            if (needDiffComparison)
            {
                var originalDiffRecord = xmlDifferences[parentXmlPath].Item1.First();
                var targetDiffRecord = xmlDifferences[parentXmlPath].Item2.First();
                diffComparer = new CollectionComparer<XName, string>(
                    originalDiffRecord.Element.Attributes().ToDictionary(
                        recordAttribute => MiscUtils.AdjustCaption(recordAttribute.Name, IgnoreNameCase, IgnoreWhiteSpaces),
                        recordAttribute => MiscUtils.AdjustCaption(recordAttribute.Value, IgnoreValueCase, IgnoreWhiteSpaces)),
                    targetDiffRecord.Element.Attributes().ToDictionary(
                        recordAttribute => MiscUtils.AdjustCaption(recordAttribute.Name, IgnoreNameCase, IgnoreWhiteSpaces),
                        recordAttribute => MiscUtils.AdjustCaption(recordAttribute.Value, IgnoreValueCase, IgnoreWhiteSpaces)));
            }

            return needDiffComparison;
        }

        /// <summary>
        /// Combine two or more path links to one path with separator as defined.
        /// </summary>
        /// <param name="links">The links to add to the path.</param>
        /// <returns>The combined path.</returns>
        public static string CombinePath(params string[] links)
        {
            if (!links.Any())
            {
                throw new ArgumentException("There must be atleast one parameter to the path-combine.");
            }

            string resultPath = links.Count() > 1
                ? string.Join(new string(PathSeparator, 1), links)
                : PathSeparator + links[0];

            return resultPath.Replace(new string(PathSeparator, 2), new string(PathSeparator, 1));
        }

        /// <summary>
        /// Split a given path and return an array result.
        /// </summary>
        /// <param name="path">The path string to split.</param>
        /// <param name="partsCount">How many first parts of the path to separate. When omitted, all path is separated.</param>
        /// <returns>The split path array.</returns>
        public static string[] SplitPath(string path, int partsCount = int.MaxValue)
        {
            var result = new List<string>();

            if (partsCount == 0)
            {
                return result.ToArray();
            }

            int lastCharCount = 0;

            foreach (var part in EnumerateSplitPath(path))
            {
                if (partsCount == 1)
                {
                    result.Add(path.Substring(lastCharCount));

                    break;
                }

                result.Add(part.Item2);
                lastCharCount = part.Item1;
                partsCount -= 1;
            }

            return result.ToArray();
        }

        /// <summary>
        /// A state machine lexer to create path parts out of the given path. Ignores anything written in between '{' and '}', unless they are quoted.
        /// </summary>
        /// <param name="path">The path string to parse.</param>
        /// <param name="ignoreEmpty">Whether to ignore empty path parts (like "//").</param>
        /// <returns>A tuple denoting the count of characters read in 'Item1' and the lexed string in 'Item2'.</returns>
        private static IEnumerable<Tuple<int, string>> EnumerateSplitPath(string path, bool ignoreEmpty = true)
        {
            var pathPart = new StringBuilder();
            bool isWithinApostroph = false;
            bool isWithinQuotes = false;
            int curlyCount = 0;
            int charCount = 0;

            foreach (char ch in path)
            {
                ++charCount;

                if (ch == PathSeparator && curlyCount == 0 &&
                    (!ignoreEmpty || pathPart.Length > 0))
                {
                    yield return new Tuple<int, string>(charCount, pathPart.ToString());

                    pathPart.Clear();

                    continue;
                }

                switch (ch)
                {
                    case '\'':
                    {
                        isWithinApostroph = !isWithinApostroph;
                        break;
                    }

                    case '"':
                    {
                        isWithinQuotes = !isWithinQuotes;
                        break;
                    }

                    case '{':
                    {
                        curlyCount += (isWithinApostroph || isWithinQuotes) ? 0 : 1;
                        break;
                    }

                    case '}':
                    {
                        curlyCount -= (isWithinApostroph || isWithinQuotes) ? 0 : 1;
                        break;
                    }
                }

                if (ch != PathSeparator || curlyCount > 0)
                {
                    pathPart.Append(ch);
                }
            }

            if (curlyCount > 0)
            {
                throw new InvalidOperationException("Lexing the XML path ended within curly braces.");
            }

            if (pathPart.Length > 0)
            {
                yield return new Tuple<int, string>(charCount, pathPart.ToString());
            }
        }

        /// <summary>
        /// Recursively creates the XML records containing representation for XML elements that contain attributes.
        /// For later to serve in comparison with another XML document.
        /// </summary>
        /// <param name="parentElements">The XML elements collection to parse.</param>
        /// <param name="parentPath">The path of this XML element</param>
        private void CreateXmlRecords(IEnumerable<XElement> parentElements, string parentPath)
        {
            if (parentElements == null)
            {
                return;
            }

            foreach (var element in parentElements)
            {
                string elementPath = CombinePath(parentPath, MiscUtils.AdjustCaption(element.Name.ToString(), IgnoreNameCase, IgnoreWhiteSpaces));
                if (KeyedXmlRecord.IsRecord(element))
                {
                    var newRecord = new KeyedXmlRecord(this, elementPath, element);

                    if (!TreeRecords.ContainsKey(newRecord.RecordPath))
                    {
                        TreeRecords.Add(newRecord.RecordPath, new ComparableSet<KeyedXmlRecord>());
                    }

                    TreeRecords[newRecord.RecordPath].Add(newRecord);
                    ElementRecords.Add(element, newRecord);
                }

                CreateXmlRecords(element.Elements(), elementPath + PathSeparator);
            }
        }
    }
}
